<?php
//error_reporting( E_ERROR );
//
//function handleError($errno, $errstr,$error_file,$error_line) {
//    file_put_contents("log.txt", "byl jsem tu");
//    $httpResponse = Nette\EnvironmenthttpResponse;
//    $httpResponse->setContentType('text/plain', 'UTF-8');
//    $output = array("status" => FALSE, "msg" => "remoteKeyNotExists");
//    $this->sendResponse(new \Nette\Application\Responses\TextResponse($output));
//}
////set error handler
//set_error_handler("handleError");

/**
 * Presenter pro vzdalenou aktualizaci
 *
 * @author David Dusek
 */

use Nette\Application\UI\Form,
    CMS\Module\Exceptions;

class RemotePresenter extends BasePresenter
{   
    const NO_KEY = -1;
    const AUTHORIZATION_FAIL = -2;
    
    public function actionDefault() {
        $post = $this->getRequest()->getPost();
        $server = new \JsonRpc\JsonRpcServer($post);
        //$server->setAuthorize(new \JsonRpc\Authorize());
        
        // registrace vzdalenych funkci
        $server->update = array($this, "update");
        $server->changeSharedKey = array($this, "changeSharedKey");
        $server->getModules = array($this, "getModules");
        $server->revert = array($this, "revert");
        $server->rollback = array($this, "rollback");
        
        $output = $server->execute();
        

//        $this->getModules("jtW2QpLL/Xodncig+nXXJTawvOAO8KKTFCOfAKtAV+I=");
//        $this->update("gUX9rFrVNT501iYl2D4ZgYRpEaFeLRpNUovGM/FTXuUxi+D/V72FA7yMVkLgBpd6XG32PjaVvcS1qR94kqnKf6S7jZkZNPye4QMMxlUPiczUwddZUa2I4mtuxaBMZ5iu"); // heslo mazac
//        $this->revert("TBBJQCWPXgaO192Omtnbjx8zkkwh9JzjnOCafrORQcI="); // heslo mazac
//        $this->rollback("TBBJQCWPXgaO192Omtnbjx8zkkwh9JzjnOCafrORQcI="); // heslo mazac
        
        $httpResponse = $this->context->httpResponse;
        $httpResponse->setContentType('text/plain', 'UTF-8');
        $this->sendResponse(new \Nette\Application\Responses\TextResponse($output));
    }
    
    public function renderDefault() {        
        \Nette\Diagnostics\Debugger::$bar = false;
        $this->terminate();
    }
        
    /**
     * Metoda provede aktualizaci teto domeny vcetne zalohy stavajici verze.
     * @param string $params 
     * @return array Vysledek aktualizace. Pod klicem status je ulozena boolean 
     * hodnota znacici vysledek operace a v pripade selhani je v polozce msg
     * chybova zprava.
     */
    public function update($params) {
        $status = array("status" => FALSE, "msg" => "unknownRemoteErr");
        
        $params = $this->decodeAndAuthorize($params);
        switch ($params) {
            case self::NO_KEY:
                $status = array("status" => FALSE, "msg" => "remoteKeyNotExists");
                break;
            case self::AUTHORIZATION_FAIL:
                $status = array("status" => FALSE, "msg" => "remoteAuthorizationFail");
                break;
            default:
                $status = $this->updateProcess($params);                
                break;
        }
        return $status;
    }
    
    /**
     * Navrat na predchozi verzi systemu.
     * @param string $params 
     * @return type 
     */
    public function revert($params) {
        $status = array("status" => FALSE, "msg" => "unknownRemoteErr");
        
        $params = $this->decodeAndAuthorize($params);
        switch ($params) {
            case self::NO_KEY:
                $status = array("status" => FALSE, "msg" => "remoteKeyNotExists");
                break;
            case self::AUTHORIZATION_FAIL:
                $status = array("status" => FALSE, "msg" => "remoteAuthorizationFail");
                break;
            default:
                $status = $this->revertProcess();
                break;
        }
        return $status;
    }
    
    /**
     * Zotaveni systemu v pripade chyby pri aktualizaci.
     * @param string $params 
     * @return type 
     */
    public function rollback($params) {
        $status = array("status" => FALSE, "msg" => "unknownRemoteErr");
        
        $params = $this->decodeAndAuthorize($params);
        switch ($params) {
            case self::NO_KEY:
                $status = array("status" => FALSE, "msg" => "remoteKeyNotExists");
                break;
            case self::AUTHORIZATION_FAIL:
                $status = array("status" => FALSE, "msg" => "remoteAuthorizationFail");
                break;
            default:
                $status = $this->rollbackProcess();                
                break;
        }
        return $status;
    }
    
    /**
     * Metoda vraci seznam nazvu vsech modulu.
     * @param string $params
     * @return array Seznam modulu na teto domene.
     */
    public function getModules($params) {
        $params = $this->decodeAndAuthorize($params);        
        $moduleList = array();
        
        if ($params != self::NO_KEY && $params != self::AUTHORIZATION_FAIL) {
            $modules = $this->core->runHook("hookModuleGetModules", null, "ModuleModule");
            if ($modules === FALSE) {
                return array();
            } else {
                foreach ($modules as $module) {
                    $moduleList[$module->name . "Module"] = $module->name;
                }
            }
        }
        return $moduleList;
    }
    
    /**
     * Metoda pro zapis noveho sdileneho klice.
     * @param type $params
     * @return array Asociativni pole s klicem status a v pripade chyby jeste 
     * polozkou errMsg. 
     */
    public function changeSharedKey($params) {                
        $params = $this->decodeAndAuthorize($params);
        switch ($params) {
            case self::NO_KEY:
                $res = array("status" => FALSE, "errMsg" => "remoteKeyNotExists");
                break;
            case self::AUTHORIZATION_FAIL:
                $res = array("status" => FALSE, "errMsg" => "remoteAuthorizationFail");
                break;
            default:
                $this->setKey($params[0]);
                $res = array("status" => TRUE);
                break;
        }
        return $res;
    }
    
    /**
     * Ziskani sdileneho klice z konfiguracniho souboru.
     * @return mixed Sdileny klic nebo false pokud neuspeje. 
     */
    private function getKey() {
        $json = @file_get_contents(MODULE_DIR . '/update/libs/config.json');
                
        if ($json === FALSE) {
            return FALSE;
        }
        
        $conf = json_decode($json, true);            
        if ($conf === NULL || !isset($conf["key"])) {
            return FALSE;
        }
        
        return $conf["key"];
    }
    
    /**
     * Ziskani slozky pro zalohu stavajici verze z konfigurace.
     * @return mixed Nazev slozky v pripade uspechu jinak false. 
     */
    private function getBackupDir() {
        $json = @file_get_contents(MODULE_DIR . '/update/libs/config.json');
                
        if ($json === FALSE) {
            return FALSE;
        }
        
        $conf = json_decode($json, true);            
        if ($conf === NULL || !isset($conf["bckdir"])) {
            return FALSE;
        }
        
        return $conf["bckdir"];
    }
    
    /**
     * Zapis sdileneho klice do konfiguracniho souboru.
     * @param string $newKey Novy sdileny klic.
     * @return boolean True pokud uspeje jinak false. 
     */
    private function setKey($newKey) {
        $filename = MODULE_DIR . '/update/libs/config.json';
        $json = @file_get_contents($filename);
                
        if ($json === FALSE) {
            return FALSE;
        }
        
        $conf = json_decode($json, true);            
        if ($conf === NULL || !isset($conf["key"])) {
            return FALSE;
        }
        
        $conf["key"] = $newKey;
        return file_put_contents($filename, json_encode($conf));
    }
    
    private function decodeAndAuthorize($params) {
        $key = $this->getKey();
        if ($key) {
            $decoded = UpdateModule::decode($params, $key);
            
            if ($decoded[(count($decoded) - 1)] == $key) {
                return $decoded;
            } else {
                return self::AUTHORIZATION_FAIL;
            }
        }
        
        return self::NO_KEY;
    }
    
    /**
     * Vlastni proces aktualizace.
     * @param array $params Parametry pro aktualizaci.
     */
    private function updateProcess($params) {
        $svnPath = $params[0];
        $svnDir = $params[1];
        $svnName = $params[2];
        $svnPass = $params[3];
        $modules = explode(';', $params[4]);       
        
        $localModulesObj = $this->core->runHook("hookModuleGetModules", null, "ModuleModule");
        if (is_array($localModulesObj)) {
            foreach ($localModulesObj as $moduleObj) {
                $localModules[] = $moduleObj->name . "Module";
            }
        } else {
            $localModules = array();
        }
        
        $modules = array_intersect($modules, $localModules);
        
        $bckupDirPath = $this->getBackupDir();
        if ($bckupDirPath === FALSE) {
            return $this->failStatus("remoteBckupDirNotFound");
        }
        
        //----------------------------------------------------------------------
        // slozka pro zalohu
        //----------------------------------------------------------------------
        $bckdir = APP_DIR . "/" . $bckupDirPath . "/" . date("Y-m-d_H-i-s");
        if (!file_exists($bckdir) && !mkdir($bckdir)) {            
            return $this->failStatus("remoteBckupDirCreateFail");
        }
        
        //----------------------------------------------------------------------
        // zaloha databaze
        //----------------------------------------------------------------------
        $databaseBckupDir = $bckdir . "/database_dump/";
        UpdateModule::backupDatabase($this->em, $databaseBckupDir);
        
        //----------------------------------------------------------------------
        // ulozeni zaznamu o aktualizaci do databaze
        //----------------------------------------------------------------------
        $updateDB = new CMS\Update\Models\Update();
        $updateDB->setModules(implode(", ", $modules));
        $updateDB->setBackupDir(realpath($bckdir));
        $updateDB->setSvnPath($svnPath);
        $updateDB->setSvnDir($svnDir);            
        $updateDB->setRemote(TRUE);            
        $this->em->persist($updateDB);
        $this->em->flush(); 
        
        //----------------------------------------------------------------------
        // zaloha aktualni verze
        //----------------------------------------------------------------------
        $destDir = $bckdir . "/modules";
        if (!file_exists($destDir)) {
            mkdir($destDir);
        }
        foreach ($modules as $module) {
            $moduleName = lcfirst(substr($module, 0, strripos($module, "Module")));
            UpdateModule::recurseCopy(MODULE_DIR . "/$moduleName", $destDir . "/$moduleName");
        }
                        
        // zaloha jadra
        $coreBckupDir = $bckdir . "/core";
        if (!file_exists($coreBckupDir)) {
            mkdir($coreBckupDir);
        }
        UpdateModule::recurseCopy(LIBS_DIR . "/CMS", $coreBckupDir . "/libs");
        UpdateModule::recurseCopy(WWW_DIR . "/CMS", $coreBckupDir . "/www");

        //----------------------------------------------------------------------
        // nahrani novych souboru
        //----------------------------------------------------------------------
        foreach ($modules as $module) {
            $moduleName = lcfirst(substr($module, 0, strripos($module, "Module")));
            UpdateModule::recurseDelete(MODULE_DIR . "/$moduleName");
        }
        
        $svnClient = new phpsvnclient($svnPath, $svnName, $svnPass);            
        foreach ($modules as $module) {
            $moduleName = lcfirst(substr($module, 0, strripos($module, "Module")));
            $svnClient->checkOut($svnDir . "/app/modules/$moduleName", MODULE_DIR . "/$moduleName");
        }
        
        UpdateModule::recurseDelete(LIBS_DIR . "/CMS");
        $svnClient->checkOut($svnDir . "/libs/CMS", LIBS_DIR . "/CMS");
        
        UpdateModule::recurseDelete(WWW_DIR . "/CMS");
        $svnClient->checkOut($svnDir . "/www/CMS", WWW_DIR . "/CMS");
        
        return array("status" => TRUE);
    }

    /**
     * Funkce pro vraceni zmen pri neuspesnem updatu. 
     */
    public function rollbackProcess() {   
        $upRep = $this->em->getRepository("\CMS\Update\Models\Update");

        $dir = $this->getBackupDir();
        $files = scandir($dir, 1);
        $bckupDir = realpath($dir . '/' . $files[0]);

        $version = $upRep->findOneBy(array("backupDir" => $bckupDir));
        // odstranime zaznam z databaze.
        if ($version !== null) {
            $this->em->remove($version);
            $this->em->flush();
        }

        // pokud jiz byly zazalohovane nejake moduly nebo jadro tak nahrajeme 
        // soubory zpet

        $modulePath = $bckupDir . "/modules";
        if (file_exists($modulePath)) {
            $dir = opendir($modulePath);

            while (false !== ($file = readdir($dir))) {
                if (( $file != '.' ) && ( $file != '..' )) {
                    if (is_dir($modulePath . '/' . $file)) {
                        UpdateModule::recurseDelete(MODULE_DIR . "/$file");
                        UpdateModule::recurseMove($modulePath . "/$file", MODULE_DIR . "/$file");
                    }
                }                    
            }
            closedir($dir);
        }

        if (file_exists($bckupDir . "/core/libs")) {
            UpdateModule::recurseDelete(LIBS_DIR . "/CMS");
            UpdateModule::recurseMove($bckupDir . "/core/libs", LIBS_DIR . "/CMS");
        }

        if (file_exists($bckupDir . "/core/www")) {
            UpdateModule::recurseDelete(WWW_DIR . "/CMS");
            UpdateModule::recurseMove($bckupDir . "/core/www", WWW_DIR . "/CMS");
        }

        // odstranime samotnou slozku se zalohou
        UpdateModule::recurseDelete($bckupDir);
        rmdir($bckupDir);
    }
    
    /**
     * Provede navrat na predchozi verzi systemu. Predchozi verze bude ziskana
     * z databaze.
     */
    public function revertProcess() {        
        $version = $this->em->getRepository("CMS\Update\Models\Update")->getLast();
        if ($version === FALSE) {
            return $this->failStatus("remoteVersionNotFound");
        }
        
        if (!$this->checkVersion($version)) {
            return $this->failStatus("remoteVersionCorrupted");
        }
        
        //----------------------------------------------------------------------
        // smazani aktualnich souboru
        //----------------------------------------------------------------------
        UpdateModule::recurseDelete(LIBS_DIR . "/CMS");
        UpdateModule::recurseDelete(WWW_DIR . "/CMS");
        $modules = explode(',', $version->modules);
        foreach ($modules as $module) {
            $moduleName = lcfirst(substr($module, 0, strrpos($module, "Module")));
            UpdateModule::recurseDelete(MODULE_DIR . "/$moduleName");
        }
        
        //----------------------------------------------------------------------
        // kopirovani novych souboru
        //----------------------------------------------------------------------
        UpdateModule::recurseCopy($version->backupDir . "/core/libs", LIBS_DIR . "/CMS");
        UpdateModule::recurseCopy($version->backupDir . "/core/www", WWW_DIR . "/CMS");
        $modules = explode(',', $version->modules);
        foreach ($modules as $module) {
            $moduleName = lcfirst(substr($module, 0, strrpos($module, "Module")));
            UpdateModule::recurseCopy($version->backupDir . "/modules/$moduleName", MODULE_DIR . "/$moduleName");
        }
        
        //----------------------------------------------------------------------
        // provedeni spkriptu pro navraceni DB
        //----------------------------------------------------------------------
        $SQLFile = $version->backupDir . "/database_dump/dump.sql";                
        $createScript = file_get_contents($SQLFile);
        $this->em->getConnection()->exec($createScript);
        
        //----------------------------------------------------------------------
        // smazani zalohy verze na kterou se vraci
        //----------------------------------------------------------------------
        UpdateModule::recurseDelete($version->backupDir);
        rmdir($version->backupDir);
        
        return array("status" => TRUE);
    }
        
    /**
     * Kontrola jestli je dana zaloha kompletni.
     * @param \CMS\Update\Models\Update $version
     * @return boolean 
     */
    private function checkVersion($version) {
        $version->backupDir;
        
        // kontrola jestli existuje zaloha vsech aktulizovanych modulu
        $modDir = $version->backupDir . "/modules";
        $modules = explode(',', $version->modules);
        foreach ($modules as $module) {
            $moduleName = lcfirst(substr($module, 0, strrpos($module, "Module")));
            if (!file_exists($modDir . "/$moduleName")) {
                return FALSE;
            }
        }
        
        // kontrola existence zalohy jadra
        if (!file_exists($version->backupDir . "/core/libs") 
                || !file_exists($version->backupDir . "/core/www")) {
            return FALSE;
        }       
        
        // kontrola existence zalohy databaze
        if (!file_exists($version->backupDir . "/database_dump/dump.sql")) {
            return FALSE;
        }
        
        return TRUE;
    }
    
    /**
     * Vytvori asociativni pole se zpravou o chybe.
     * @param string $msg Zprava s upresnenim chyby.
     * @return array Asociativni pole kde pod klicem status je ulozeno FALSE
     * a pod klicem msg zprava s informaci o vynikle chybe. 
     */
    private function failStatus($msg) {
        return array(
                    "status" => FALSE, 
                    "msg" => $msg
               );
    }
   
}
